<cfcomponent name="PNGmanip" hint="Manipulate PNG files" namespace="com.nowrists" output="false">
	<!--- 
	PNG Manipulation component
	Original coding by Rick Osborne (rickosborne.org), October 2007
	License: Mozilla Public License v1.1 -- http://www.mozilla.org/MPL/MPL-1.1.html
	
	DO NOT USE THIS CODE WITHOUT UNDERSTANDING THE LICENSE
	--->
	<!--- Public --->
	<cfset this.lastError="">
	<cfset this.lastCatch="">
	<cfset this.maxFileSize=3000000>
	<cfset this.maxChunkSize=300000>
	<cfset this.maxChunkCount=250>
	<cfset this.isPngFile=false>
	<cfset this.warnings=queryNew("FileOffset,Chunk,ChunkOffset,Warning")>
	<!--- Private --->
	<cfset variables.chunks=queryNew("Type,DataOffset,DataLength,CRC,ActualCRC,CopySafe,Reserved,Private,Ancillary,WillExport")>
	<cfset variables.pngSignature=listToArray("137,80,78,71,13,10,26,10")>
	<cfset variables.chunkData=arrayNew(1)>
	
	<cffunction name="init" output="false" returntype="PNGmanip" access="public" description="Initialization">
		<cfargument name="fileName" type="string" required="false" default="">
		<cfif arguments.fileName NEQ "">
			<cfset readPNG(arguments.filename)>
		</cfif>
		<cfreturn this>
	</cffunction>
	<cffunction name="readPNG" output="false" returntype="boolean" access="public" description="Load a PNG file">
		<cfargument name="fileName" type="string" required="true">
		<cfset var ret=false>
		<cfset var byteCount=0>
		<cfset var inFile="">
		<cfset var i=0>
		<cfset var dirFile="">
		<cfset var chunkCount=0>
		<cfset var offset=0>
		<cfset var startTicks=getTickCount()>
		<cfset variables.chunks=queryNew(variables.chunks.columnList)>
		<cftry>
			<cfdirectory action="LIST" directory="#getDirectoryFromPath(arguments.fileName)#" filter="#getFileFromPath(arguments.fileName)#" name="dirFile">
			<cfif dirFile.recordCount neq 1>
				<cfthrow message="The file could not be found">
			<cfelseif dirFile.Type[1] NEQ "FILE">
				<cfthrow message="The file is not a file" detail="It looks like the file is a #dirFile.Type[1]#, not a FILE.">
			<cfelseif dirFile.Size[1] gt this.maxFileSize>
				<cfthrow message="The file was too big" detail="A limit of #numberFormat(this.maxFileSize)# bytes has been set, and the target file is #numberFormat(dirFile.Size[1])# bytes.">
			</cfif>
			<cfset this.fileSize=dirFile.size[1]>
			<cfset inFile=createObject("java","java.io.FileInputStream").init(javaCast("string",arguments.fileName))>
			<!--- Java's lack of unsigned bytes makes this a complete cluster --->
			<cfif isObject(inFile)>
				<cfset inFile=createObject("java","java.io.DataInputStream").init(inFile)>
			</cfif>
			<cfif isObject(inFile)>
				<cfloop index="i" from="1" to="#arrayLen(variables.pngSignature)#">
					<cfif inFile.readUnsignedByte() neq variables.pngSignature[i]>
						<cfset inFile.close()>
						<cfthrow message="Not a valid PNG file" detail="The file did not begin with a PNG signature (byte #i# mismatch)">
					</cfif>
				</cfloop>
				<cfset chunkCount=0>
				<cfset offset=arrayLen(pngSignature)>
				<cfloop condition="(offset gt 0) and (offset lt this.fileSize) and (chunkCount lte this.maxChunkCount)">
					<cfset offset=readNextChunk(inFile,offset)>
					<cfset chunkCount=chunkCount+1>
				</cfloop>
				<cfset inFile.close()>
				<cfif (offset eq 0) or (chunkCount lt 3)><!--- Must have IHDR, IDAT, IEND --->
					<cfset ret=false>
				<cfelse>
					<cfif variables.chunks.type[1] eq "IHDR">
						<cfif variables.chunks.dataLength[1] eq 13>
							<cfset this.imageWidth=0>
							<cfloop index="i" from="1" to="4">
								<cfset this.imageWidth=(this.imageWidth * 256) + unsign(variables.chunkData[1][i])>
							</cfloop>
							<cfset this.imageHeight=0>
							<cfloop index="i" from="5" to="8">
								<cfset this.imageHeight=(this.imageHeight * 256) + unsign(variables.chunkData[1][i])>
							</cfloop>
							<cfset this.imageBitDepth=unsign(variables.chunkData[1][9])>
							<cfset this.imageColorType=unsign(variables.chunkData[1][10])>
							<cfset this.imageCompressionMethod=unsign(variables.chunkData[1][11])>
							<cfset this.imageFilterMethod=unsign(variables.chunkData[1][12])>
							<cfset this.imageInterlaceMethod=unsign(variables.chunkData[1][13])>
							<cfswitch expression="#this.imageColorType#">
								<cfcase value="0,4" delimiters=","><cfset this.imageColorTypeName="Greyscale"></cfcase>
								<cfcase value="2,6" delimiters=","><cfset this.imageColorTypeName="Truecolour"></cfcase>
								<cfcase value="3"><cfset this.imageColorTypeName="Indexed-colour"></cfcase>
								<cfdefaultcase><cfset this.imageColorTypeName=""></cfdefaultcase>
							</cfswitch>
							<cfif (this.imageColorType eq 4) or (this.imageColorType eq 6)>
								<cfset this.imageAlphaSamples=true>
							<cfelse>
								<cfset this.imageAlphaSamples=false>
							</cfif>
							<cfset ret=true>
						<cfelse>
							<cfset addWarning("Header chunk is corrupt",variables.chunks.dataOffset[1]-4,variables.chunks.type[1],-4)>
							<cfset ret=false>
						</cfif>
					<cfelse>
						<cfset addWarning("First chunk is not IHDR",variables.chunks.dataOffset[1]-4,variables.chunks.type[1],-4)>
						<cfset ret=false>
					</cfif>
				</cfif>
				<cfset this.chunks=variables.chunks>
			</cfif>
		<cfcatch>
			<cfset this.lastError=cfcatch.message>
			<cfset this.lastCatch=cfcatch>
		</cfcatch>
		</cftry>
		<cfset this.isPngFile=ret>
		<cfset this.loadTime=getTickCount()-startTicks>
		<cfreturn ret>
	</cffunction>
	<cffunction name="unsign" access="private" returntype="numeric" output="false" description="Convert a signed byte to an unsigned int">
		<cfargument name="byte" type="numeric" required="true">
		<cfset var ret=arguments.byte>
		<cfif ret lt 0>
			<cfset ret=ret+256>
		</cfif>
		<cfreturn ret>
	</cffunction>
	<cffunction name="arrayOfBytes" access="private" returntype="any" output="false" description="Return a primitive array of bytes">
		<cfargument name="size" type="numeric" required="true">
		<cfreturn createObject("java","java.lang.reflect.Array").newInstance(createObject("java", "java.lang.Byte").TYPE, javaCast("int",arguments.size))>
	</cffunction>
	<cffunction name="readNextChunk" access="private" returntype="numeric" output="false" description="Read the next PNG chunk and return the new file offset">
		<cfargument name="inFile" type="any" required="true">
		<cfargument name="offset" type="numeric" required="true">
		<cfset var ret=0>
		<cfset var off=arguments.offset>
		<cfset var chunkLen=0>
		<cfset var chunkType="">
		<cfset var c=0>
		<cfset var nonASCII=false>
		<cfset var crc="">
		<cfset var actualCRC="">
		<cfset readBytes=0>
		<cfset buf="">
		<!--- Why doesn't Java have readUnsignedInt? --->
		<cfset chunkLen=(inFile.readUnsignedShort() * 65536) + inFile.readUnsignedShort()>
		<cfif chunkLen gte 0>
			<cfloop index="off" from="#int(arguments.offset+4)#" to="#int(arguments.offset+7)#">
				<cfset c=inFile.readUnsignedByte()>
				<cfif c lt 0>
					<cfset addWarning("Truncated file in mid-chunk-type",off)>
					<cfreturn 0>
				</cfif>
				<cfset chunkType=chunkType & chr(c)>
				<cfif (c lt 65) or (c gt 122) or ((c gt 90) and (c lt 97))>
					<cfset nonASCII=true>
				</cfif>
			</cfloop>
			<cfif nonASCII>
				<cfset addWarning("Non-ASCII data in chunk name", arguments.offset+4, chunkType, -4)>
			</cfif>
			<cfif chunkLen lte this.maxChunkSize>
				<cfset readBytes=0>
				<cfif chunkLen gt 0>
					<cfset buf=arrayOfBytes(chunkLen)>
					<cfset readBytes=inFile.read(buf)>
				</cfif>
				<cfif readBytes eq chunkLen>
					<cfset off=arguments.offset+8+chunkLen>
					<cfset crc="">
					<cfloop from="#off#" to="#int(off+3)#" index="off">
						<cfset c=inFile.readUnsignedByte()>
						<cfif c lt 0>
							<cfset addWarning("Truncated file in mid-crc",off,chunkType,chunkLen+off)>
							<cfreturn 0>
						</cfif>
						<cfset crc=crc & right("00" & formatBaseN(c,16),2)>
					</cfloop>
					<cfset actualCRC=addChunk(chunkType, arguments.offset+8, chunkLen, crc, true, buf)>
					<cfif crc neq actualCRC>
						<cfset addWarning("CRC mismatch (#crc# neq #actualcrc#)",off,chunkType,chunkLen)>
					</cfif>
					<cfset ret=off>
				<cfelse>
					<cfset addWarning("Not enough room left in the file for a CRC (mangled or truncated file?)", off, chunkType, chunkLen)>
					<cfset ret=0>
				</cfif>
			<cfelse>
				<cfset addWarning("Chunk too big (#chunkLen# gt #this.maxChunkSize#)",arguments.offset,chunkType,-8)>
				<cfset ret=0>
			</cfif>
		<cfelse>
			<cfset addWarning("Not enough room left in the file for a chunk (mangled or truncated file?)",arguments.offset)>
			<cfset ret=0>
		</cfif>
		<cfreturn ret>
	</cffunction>
	<cffunction name="appendTextChunk" access="public" output="false" returntype="boolean" description="Add a new text chunk to the end of the PNG file">
		<cfargument name="message" required="true" type="string">
		<cfargument name="keyword" required="false" type="string" default="Comment">
		<cfset var kw=Left(trim(arguments.keyword),79)>
		<cfset var msg=trim(arguments.message)>
		<cfset var data=arrayNew(1)>
		<cfset var i=0>
		<cfset var j=0>
		<cfif kw eq ""><cfset kw="Comment"></cfif>
		<cfset arrayResize(data, len(kw) + len(msg) + 1)>
		<cfloop index="i" from="1" to="#len(kw)#">
			<cfset data[i]=asc(mid(kw,i,1))>
		</cfloop>
		<cfset j=len(kw)+1>
		<cfset data[j]=0>
		<cfloop index="i" from="1" to="#len(msg)#">
			<cfset data[j+i]=asc(mid(msg,i,1))>
		</cfloop>
		<cfreturn appendChunk("tEXt",data)>
	</cffunction>
	<cffunction name="appendChunk" access="private" output="false" returntype="boolean" description="Add a new chunk to right before the end of the PNG file">
		<cfargument name="type" type="string" required="true">
		<cfargument name="bytes" type="any" required="true">
		<cfset var i=0>
		<cfset var colName="">
		<cfset var l=0>
		<cfset var data=arguments.bytes>
		<cfset var dl=0>
		<cfset var lastOffset=0>
		<cfset var crc="">
		<cfif isSimpleValue(data)>
			<cfset data=arrayNew(1)>
			<cfset arrayResize(data, len(arguments.bytes))>
			<cfloop from="1" to="#len(arguments.bytes)#" index="i">
				<cfset data[i]=asc(mid(arguments.bytes,i,1))>
			</cfloop>
		</cfif>
		<cfif NOT isArray(data)>
			<cfreturn false>
		</cfif>
		<cfset lastOffset=variables.chunks.dataOffset[variables.chunks.recordCount]>
		<cfset variables.chunks.dataOffset[variables.chunks.recordCount]=lastOffset+dl+12>
		<cfset crc=addChunk(arguments.type, lastOffset, arrayLen(data), "", true, data)>
		<!--- swap the last two chunks in the query --->
		<cfset l=variables.chunks.recordCount>
		<cfloop list="#variables.chunks.columnList#" index="colName">
			<cfset i=variables.chunks[colName][l]>
			<cfset querySetCell(variables.chunks,colName,variables.chunks[colName][l-1],l)>
			<cfset querySetCell(variables.chunks,colName,i,l-1)>
		</cfloop>
		<!--- Swap chunk data buffers --->
		<cfset i=variables.chunkData[l]>
		<cfset variables.chunkData[l]=variables.chunkData[l-1]>
		<cfset variables.chunkData[l-1]=i>
		<cfreturn true>
	</cffunction>
	<cffunction name="addChunk" access="private" output="false" returntype="string" description="Add a new chunk to the very end of the PNG file">
		<cfargument name="Type" type="string" required="false" default="">
		<cfargument name="DataOffset" type="numeric" required="false" default="0">
		<cfargument name="DataLength" type="numeric" required="false" default="0">
		<cfargument name="CRC" type="string" required="false" default="">
		<cfargument name="WillExport" type="boolean" required="false" default="true">
		<cfargument name="Data" type="any" required="false" default="">
		<cfset var colName="">
		<cfset var l=variables.chunks.recordCount+1>
		<cfset var actualCRC="">
		<cfset queryAddRow(variables.chunks)>
		<cfloop list="#variables.chunks.columnList#" index="colName">
			<cfif structKeyExists(arguments, colName)>
				<cfset querySetCell(variables.chunks, colName, arguments[colName], l)>
			</cfif>
		</cfloop>
		<cfset variables.chunkData[l]=arguments.data>
		<cfif isSimpleValue(arguments.data)><cfset arguments.data=arrayNew(1)></cfif>
		<cfset actualCRC=calculateCRC(arguments.data, 1, arguments.dataLength, arguments.type)>
		<cfset querySetCell(variables.chunks, "ActualCRC", actualCRC, l)>
		<cfif arguments.CRC eq "">
			<cfset querySetCell(variables.chunks, "CRC", actualCRC, l)>
		</cfif>
		<cfset querySetCell(variables.chunks, "CopySafe", NOT bitSHRN(bitAnd(32,asc(mid(arguments.Type,4,1))),5), l)>
		<cfset querySetCell(variables.chunks, "Reserved", NOT NOT bitSHRN(bitAnd(32,asc(mid(arguments.Type,3,1))),5), l)>
		<cfset querySetCell(variables.chunks, "Private", NOT NOT bitSHRN(bitAnd(32,asc(mid(arguments.Type,2,1))),5), l)>
		<cfset querySetCell(variables.chunks, "Ancillary", NOT NOT bitSHRN(bitAnd(32,asc(mid(arguments.Type,1,1))),5), l)>
		<cfreturn actualCRC>
	</cffunction>
	<cffunction name="consolidateDataChunks" access="public" output="false" returntype="boolean" description="Consolidate all of the IDAT chunks into one big chunk">
		<cfset var firstChunk=0>
		<cfset var chunkIDs=arrayNew(1)>
		<cfset var newLength=0>
		<cfset var stream="">
		<cfset var crc="">
		<cfset var startTicks=getTickCount()>
		<cfset j=0>
		<cfloop query="variables.chunks">
			<cfif type EQ "IDAT">
				<cfif firstChunk eq 0>
					<cfset firstChunk=currentRow>
				</cfif>
				<cfset newLength=newLength+DataLength>
				<cfset arrayAppend(chunkIDs, currentRow)>
			</cfif>
		</cfloop>
		<cfif arrayLen(chunkIDs) gt 1>
			<cfset stream=createObject("java","java.io.ByteArrayOutputStream").init(javaCast("int",newLength))>
			<cfloop from="1" to="#arrayLen(chunkIDs)#" index="j">
				<cfset i=chunkIDs[j]>
				<cfif isArray(variables.chunkData[i])>
					<cfset stream.write(variables.chunkData[i])>
				</cfif>
				<cfif i gt firstChunk>
					<cfset querySetCell(variables.chunks, "WillExport", false, i)>
					<cfset variables.chunkData[i]="">
				</cfif>
			</cfloop>
			<cfset variables.chunkData[firstChunk]=stream.toByteArray()>
			<cfset querySetCell(variables.chunks, "DataLength", newLength, firstChunk)>
			<cfset querySetCell(variables.chunks, "CRC", calculateCRC(variables.chunkData[firstChunk], 1, newLength, "IDAT"), firstChunk)>
			<cfset querySetCell(variables.chunks, "ActualCRC", variables.chunks.crc[firstChunk], firstChunk)>
		</cfif>
		<cfset this.consolidationTime=getTickCount()-startTicks>
		<cfreturn true>
	</cffunction>
	<cffunction name="removeChunkTypes" access="public" output="false" returntype="numeric" description="Remove chunks of the specified type, returning the count removed">
		<cfargument name="typeList" type="string" required="true">
		<cfargument name="caseInsensitive" type="boolean" default="true" required="false">
		<cfset ret=0>
		<cfloop query="variables.chunks">
			<cfif (arguments.caseInsensitive and (listFindNoCase(arguments.typeList, Type) gt 0)) or (listFind(arguments.typeList, Type) gt 0)>
				<cfset querySetCell(variables.chunks, "WillExport", false, currentRow)>
				<cfset variables.chunkData[currentRow]="">
				<cfset ret=ret+1>
			</cfif>
		</cfloop>
		<cfreturn ret>
	</cffunction>
	<cffunction name="calculateCRC" access="private" output="false" returntype="string" description="Calculate a CRC for the bytes of an array">
		<cfargument name="bytes" type="array" required="true">
		<cfargument name="startOffset" type="numeric" required="false" default="1">
		<cfargument name="count" type="numeric" required="false" default="-1">
		<cfargument name="prefix" type="string" required="false" default="">
		<cfset var c=arguments.count>
		<cfset var off=arguments.startOffset>
		<cfset var startOff=arguments.startOffset>
		<cfset var l=arrayLen(arguments.bytes)-startOff+1><!--- bytes left from start to end --->
		<cfset var endOff=0>
		<cfset var ffffffff=inputBaseN("ffffffff",16)>
		<cfset var ff=inputBaseN("ff",16)>
		<cfset var crc=ffffffff>
		<cfset var magic=inputBaseN("edb88320",16)>
		<cfset var table=arrayNew(1)>
		<cfset var n=0>
		<cfset var k=0>
		<cfif c lt 0>
			<cfset c=l><!--- do the entire thing --->
		</cfif>
		<cfset l=min(l,c)><!--- don't go past the end --->
		<cfset endOff=startOff+l-1>
		<cfif (startOff lte endOff) or (len(arguments.prefix) gt 0)>
			<cfif structKeyExists(variables, "crcTable")>
				<cfset table=variables.crcTable>
			<cfelse>
				<!--- build our CRC lookup table --->
				<cfset arrayResize(table,256)>
				<cfloop index="n" from="1" to="256">
					<cfset c=n-1>
					<cfloop index="k" from="0" to="7">
						<cfif bitAnd(c,1)>
							<cfset c=bitXor(magic,bitSHRN(c,1))>
						<cfelse>
							<cfset c=bitSHRN(c,1)>
						</cfif>
					</cfloop>
					<cfset table[n]=c>
				</cfloop>
				<cfset variables.crcTable=table>
			</cfif>
			<cfloop index="off" from="1" to="#len(arguments.prefix)#">
				<cfset crc=bitXor(table[incrementValue(bitAnd(bitXor(crc,asc(mid(arguments.prefix,off,1))),ff))],bitSHRN(crc,8))>
			</cfloop>
			<cfloop index="off" from="#startOff#" to="#endOff#">
				<cfset crc=bitXor(table[incrementValue(bitAnd(bitXor(crc,arguments.bytes[off]),ff))],bitSHRN(crc,8))>
			</cfloop>
		</cfif>
		<cfreturn right("00000000" & formatBaseN(bitXor(crc,ffffffff),16),8)>
	</cffunction>
	<cffunction name="addWarning" access="private" output="false" description="Add a warning">
		<cfargument name="warning" type="string" required="true">
		<cfargument name="fileOffset" type="numeric" required="false" default="0">
		<cfargument name="chunk" type="string" required="false" default="">
		<cfargument name="chunkOffset" type="numeric" required="false" default="0">
		<cfset var argName="">
		<cfset queryAddRow(this.warnings)>
		<cfloop list="#this.warnings.columnList#" index="argName">
			<cfif structKeyExists(arguments, argName)>
				<cfset querySetCell(this.warnings, argName, arguments[argName], this.warnings.recordCount)>
			</cfif>
		</cfloop>
	</cffunction>
	<cffunction name="updateTime" access="public" output="false" description="Timestamp the last modification time of the PNG file">
		<cfset var utc=dateConvert("local2utc",Now())>
		<cfset var utcYear=Year(utc)>
		<cfset removeChunkTypes("tIME")>
		<cfset appendChunk("tIME",listToArray("#Int(utcYear/256)#,#Int(utcYear MOD 256)#,#dateFormat(utc,'m,d')#,,#timeFormat(utc,'H,m,s')#"))>
	</cffunction>
	<cffunction name="getFileSize" access="public" output="false" description="Return the total file size">
		<cfset var ret=arrayLen(variables.pngSignature)>
		<cfloop query="variables.chunks">
			<cfif WillExport>
				<cfset ret=ret+12+DataLength>
			</cfif>
		</cfloop>
		<cfreturn ret>
	</cffunction>
	<cffunction name="writePNG" access="public" output="false" description="Write the PNG to a file">
		<cfargument name="fileName" type="string" required="true">
		<cfset var data=createObject("java","java.io.FileOutputStream")>
		<cfset var i=0>
		<cfset var j=0>
		<cfset var buf=arrayNew(1)>
		<cfset data.init(javaCast("string",arguments.fileName))>
		<cfloop from="1" to="#arrayLen(variables.pngSignature)#" index="i">
			<cfset data.write(javaCast("int",variables.pngSignature[i]))>
		</cfloop>
		<cfset arrayResize(buf,4)>
		<cfloop query="variables.chunks">
			<cfif WillExport>
				<cfset j=DataLength>
				<!--- length --->
				<cfloop from="4" to="1" index="i" step="-1">
					<cfset buf[i]=j MOD 256>
					<cfset j=int(j/256)>
				</cfloop>
				<cfloop from="1" to="4" index="i">
					<cfset data.write(javaCast("int",buf[i]))>
				</cfloop>
				<!--- type --->
				<cfloop from="1" to="4" index="i">
					<cfset data.write(javaCast("int",asc(mid(Type,i,1))))>
				</cfloop>
				<!--- data --->
				<cfif isArray(variables.chunkData[currentRow]) and (arrayLen(variables.chunkData[currentRow]) gt 0)>
					<cfset data.write(variables.chunkData[currentRow])>
				</cfif>
				<!--- crc --->
				<cfloop from="1" to="4" index="i">
					<cfset data.write(javaCast("int",javaCast("int",inputBaseN(mid(crc,(2*i)-1,2),16))))>
				</cfloop>
			</cfif>
		</cfloop>
		<cfset data.close()>
	</cffunction>
</cfcomponent>